- class和category的数据结构
- 为什么category只能添加方法,不能添加属性?
- category为何不要覆盖父类方法?
- 多个category覆盖同名方法的执行顺序
- category与extension的区别
- 关联对象和其他补充
class和category的数据结构
在学习runtime时,可以知道,OC的所有数据结构,包括对象、类、方法、协议等等,都是C的结构体。
类的实例,即对象,主要只包含了一个isa指针,指向其所属的class。而class的isa指针则指向其元类(meta class),元类的isa指向根原类,根元类isa指向自身。
来一张转烂了的图,顺便说明isa和super指针的指向关系:
class的主要数据结构(元类与其相同):
|
|
简要说明下:
- 根据class的数据结构,即可了解runtime的主要功能:可以在运行时获取类的相关信息,包括实例变量、方法列表、协议列表等,且可以通过相关api对各部分进行增删改查等操作(甚至是修改method的实现)。
- OC中的方法调用,并非在编译期确定调用地址,而是在运行时才会确定调用方法的真正地址(通过objc_msgsend函数进行方法调用)。子类的方法调用,会在methodlist中查找,未找到后,会在父类的methodlist中继续查找,直到找到后调用(未找到后进入消息转发)。找到后,会将此method的地址放入cache中,以加快访问。
我们主要讨论的category,其结构如下:
|
|
为什么category只能添加方法,不能添加属性?
从category的结构中可以看出,由于内部只保存了属性,并不包含ivar(成员变量才能保存属性的值),即只生成了setter和getter方法的声明,没有对应实现,更没有对应的成员变量保存。所以在category中添加属性,需要使用runtime的相关对象(associate_object)进行。
而且对于category来说,添加的方法会在编译后添加到主类的方法列表中(实例方法添加到主类,类方法添加到主类所属的元类中)。
category为何不要覆盖父类方法?
我们都知道,在category中覆盖父类方法时,系统会弹出警告(主类也实现了此方法),且调用原方法时,系统会执行category的实现,从而导致原始方法实现被覆盖。这里是为什么?
我们知道,由于category的方法在runtime加载后会添加到本类的方法表中,方法表保存的是方法的SEL,可以推测:
- 原SEL被覆盖
- 两个同名方法都被加入到方法表中,调用时只执行了category的
另一个问题是,runtime在加载时,对于本类和category的加载顺序是什么。
参照以下代码可以验证:
|
|
我们可以在自己的测试类中,实例化TestClassForCategory并调用testMethod方法,可以看到输出结果:
2018-03-27 07:55:10.278999+0800 Runtime_Learning2[3506:4499896] load === TestClassForCategory
2018-03-27 07:55:10.280645+0800 Runtime_Learning2[3506:4499896] load === TestClassForCategory+Category1
2018-03-27 07:55:14.702309+0800 Runtime_Learning2[3506:4499896] category1—-
由此可以证明,runtime加载时,首先加载主类,然后加载category。
我们通过runtime的api来获取一下TestClassForCategory的方法表:
|
|
执行结果如下:
2018-03-27 07:55:10.683704+0800 Runtime_Learning2[3506:4499896] method - testMethod
2018-03-27 07:55:10.683855+0800
Runtime_Learning2[3506:4499896] category1—-
2018-03-27 07:55:10.683965+0800 Runtime_Learning2[3506:4499896] method - testMethod
2018-03-27 07:55:10.684055+0800 Runtime_Learning2[3506:4499896] instance method
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 >可以看到,**在主类的方法表中,果然保存了主类和分类两个SEL,且方法实现均可以被调用**。联系着load方法的执行顺序,我们可以知道,**class在方法列表中添加方法时,是依照“头部插入”的方式(“头插法”)来修改链表的**。原理如下(转自[iOS OC中分类Category实现原理](http://blog.sina.com.cn/s/blog_14679a7d20102xae4.html)):![img](http://img.blog.csdn.net/20180131104524542?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2hpaHVib2tl/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)由此可见,当分类中覆盖了主类的方法后,**由于分类的方法在原方法前部,导致调用方法时,系统查找到第一个方法后,直接返回执行,本类方法不会执行**。所以一般不建议在分类中覆盖主类方法。特别是给系统类添加分类时,由于本类或其父类可能会有相同的方法声明,导致被分类覆盖。为避免此类问题的出现,可以在分类中声明方法时,添加自定义的方法前缀。### 多个category覆盖同名方法的执行顺序我们知道,在类的方法表中添加方法使用的是“头插法”,但当给主类添加多个分类时,分类方法的加载顺序又是如何呢?根据代码来看:``` C// 在以上代码的基础上再添加第二个category#import "TestClassForCategory.h"@interface TestClassForCategory (Category2)@end#import "TestClassForCategory+Category2.h"@implementation TestClassForCategory (Category2)+ (void)load {NSLog(@"load === TestClassForCategory+Category2");}- (void)testMethod {// category的方法会添加到NSLog(@"category2----");}@end```执行结果如下:>> 2018-03-27 10:58:50.708610+0800 Runtime_Learning2[4434:4568830] load === TestClassForCategory> 2018-03-27 10:58:50.711459+0800 Runtime_Learning2[4434:4568830] load === TestClassForCategory+Category1> 2018-03-27 10:58:50.712078+0800 Runtime_Learning2[4434:4568830] load === TestClassForCategory+Category2> 2018-03-27 10:58:50.927215+0800 Runtime_Learning2[4434:4568830] method - testMethod> 2018-03-27 10:58:50.927375+0800 Runtime_Learning2[4434:4568830] category2----> 2018-03-27 10:58:50.927585+0800 Runtime_Learning2[4434:4568830] method - testMethod> 2018-03-27 10:58:50.928280+0800 Runtime_Learning2[4434:4568830] category1----> 2018-03-27 10:58:50.928472+0800 Runtime_Learning2[4434:4568830] method - testMethod> 2018-03-27 10:58:50.928653+0800 Runtime_Learning2[4434:4568830] instance method2018-03-27 10:58:53.851391+0800 Runtime_Learning2[4434:4568830] category2—-
可以看到,最后加载的category2,则执行方法指向的是category2的方法。原因在哪呢?
在工程TARGETS -> Build Phases -> Compile Sources中,我们可以看到,自上而下,编译资源的顺序是:Category1.m、Category2.m,我们可以自己上下调整编译顺序并执行,结果显而易见:
多个category的加载顺序(也是方法的覆盖顺序)是依照Compile Sources的顺序自上而下确定的。
注意,返回值不同的方法也会覆盖,因为SEL与返回值无关。
所以,多个分类同时覆盖主类的方法时,执行结果无法直接确定,可能会出现不可预估的问题。
category与extension的区别
一般来说,我们都会认为class extension(类的扩展)是匿名的category,因为语法详尽,且功能相似,但实际上来说,二者可以说是完全不同。
extension:
- 它是class的一部分,与interface、implementation三者共同组成class,声明周期与class相同。
- 分类是在编译期确定的,内部声明的属性和方法在编译期直接组成class结构体的数据(属性会自动生成ivar并合成setter和getter,分别添加到class的ivar_list和method_list中)。
- 分类声明的方法,实现必须在class的implementation中,所以没有本类的源代码无法添加extension。
category:
- 与class本体无关,可以单独存在。
- 扩展是在编译期编译,在运行时加载到类中。category由于其数据结构所限,只能添加方法实现、属性声明和协议实现,内部不包含ivar成员变量,所以无法保存数据(声明的属性不能生成对应的ivar,也不能合成出setter和getter的实现)。
- 编译期由于class结构布局和数据已经确定,在运行时,category只能将方法添加到class的方法链表中,其他东西无法修改(class的实例大小等在编译期已确定,无法修改)。
关联对象和其他补充
由于在category中添加属性,需要使用关联对象(associate object)进行添加和访问,那关联对象的数据保存在哪里呢?
所有的关联对象都由AssociationsManager进行统一管理(添加、删除等),保存在全局map中,其中key是这个相关对象的指针地址,value则是单独的一个AssociationsHashMap,里面保存着key-value对。
提问:在class的load方法中,可以调用Category的方法吗?
答:可以。runtime加载class和category等是在load方法调用前执行的,此时Category的方法已经加载到class的方法列表中,可以直接调用。提问:当Category已经覆盖了Class中的方法时,如何调用Class中的原方法?
答:由于使用“头插法”,Category的方法已经在Class的方法列表头部。系统调用方法时是从头开始检索,查到对应Method后即跳转IMP进行调用。我们可以使用runtime的api手动遍历Class的方法表,找到最后一个同名方法,直接调用IMP。扩展一个类的方式:
- 子类化
- 使用category
- 使用协议抽象(如UITableView的delegate和data source,可以给具有显示功能的类进行扩展)
参考内容: